﻿//==============================================================================
// File: ResxExtension.cs
// Created: 2010-06-07
// Author: Piotr Włodek, Arkadiusz Świerczek
//==============================================================================
// This file is a part of MAMMOTH project.
// Copyright (C) 2010 AGH University of Science and Technology, Krakow.
// https://caribou.iisg.agh.edu.pl/trac/mammoth
//
// Mammoth is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// Mammoth is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public License
// along with Mammoth. If not, see http://www.gnu.org/licenses/.
//==============================================================================

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Resources;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Markup;
using System.Windows.Media.Imaging;

namespace WpfRcp.Core.Common.Localization
{
    /// <summary>
    /// Defines the handling method for the <see cref="ResxExtension.GetResource"/> event
    /// </summary>
    /// <param name="resxName">The name of the resx file</param>
    /// <param name="key">The resource key within the file</param>
    /// <param name="culture">The culture to get the resource for</param>
    /// <returns>The resource</returns>
    public delegate object GetResourceHandler(string resxName, string key, CultureInfo culture);

    /// <summary>
    /// A markup extension to allow resources for WPF Windows and controls to be retrieved
    /// from an embedded resource (resx) file associated with the window or control
    /// </summary>
    /// <remarks>
    /// 
    /// </remarks>
    [MarkupExtensionReturnType(typeof(string))]
    public class ResxExtension : ManagedMarkupExtension
    {
        #region XAML Support

        public static string GetGlobalResxName(DependencyObject obj)
        {
            return (string)obj.GetValue(GlobalResxNameProperty);
        }

        public static void SetGlobalResxName(DependencyObject obj, string value)
        {
            obj.SetValue(GlobalResxNameProperty, value);
        }

        public static readonly DependencyProperty GlobalResxNameProperty =
            DependencyProperty.RegisterAttached("GlobalResxName", typeof(string), typeof(ResxExtension),
                new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits));

        #endregion

        #region Member Variables

        /// <summary>
        /// The resource manager to use for this extension.  Holding a strong reference to the
        /// Resource Manager keeps it in the cache while ever there are ResxExtensions that
        /// are using it.
        /// </summary>
        private ResourceManager m_ResourceManager;

        /// <summary>
        /// Cached resource managers
        /// </summary>
        private static readonly Dictionary<string, WeakReference> ResourceManagers = new Dictionary<string, WeakReference>();

        /// <summary>
        /// The manager for resx extensions
        /// </summary>
        private static readonly MarkupExtensionManager ResxMarkupManager = new MarkupExtensionManager(40);

        #endregion

        #region Public Interface

        /// <summary>
        /// This event allows a designer or preview application (such as Globalizer.NET) to
        /// intercept calls to get resources and provide the values instead dynamically
        /// </summary>
        public static event GetResourceHandler GetResource;

        /// <summary>
        /// Create a new instance of the markup extension
        /// </summary>
        public ResxExtension()
            : base(ResxMarkupManager)
        {
        }

        /// <summary>
        /// Create a new instance of the markup extension.
        /// </summary>
        /// <param name="key">The key used to get the value from the resources.</param>
        /// <remarks>
        /// The default value for the property (can be null).  This is useful for non-string
        /// that properties that may otherwise cause page load errors if the resource is not
        /// present for some reason (eg at design time before the assembly has been compiled)
        /// </remarks>
        public ResxExtension(string key)
            : base(ResxMarkupManager)
        {
            Key = key;
        }

        /// <summary>
        /// Create a new instance of the markup extension
        /// </summary>
        /// <param name="resxName">The fully qualified name of the embedded resx (without .resources)</param>
        /// <param name="key">The key used to get the value from the resources</param>
        /// <param name="defaultValue">
        /// The default value for the property (can be null).  This is useful for non-string
        /// that properties that may otherwise cause page load errors if the resource is not
        /// present for some reason (eg at design time before the assembly has been compiled)
        /// </param>
        public ResxExtension(string resxName, string key, string defaultValue)
            : base(ResxMarkupManager)
        {
            ResxName = resxName;
            Key = key;
            
            if (!string.IsNullOrEmpty(defaultValue))
            {
                DefaultValue = defaultValue;
            }
        }

        /// <summary>
        /// The fully qualified name of the embedded resx (without .resources) to get
        /// the resource from
        /// </summary>
        public string ResxName { get; set; }

        /// <summary>
        /// The name of the resource key
        /// </summary>
        public string Key { get; set; }

        /// <summary>
        /// The default value to use if the resource can't be found
        /// </summary>
        /// <remarks>
        /// This particularly useful for properties which require non-null
        /// values because it allows the page to be displayed even if
        /// the resource can't be loaded
        /// </remarks>
        public string DefaultValue { get; set; }

        /// <summary>
        /// Return the MarkupManager for this extension
        /// </summary>
        public static MarkupExtensionManager MarkupManager
        {
            get { return ResxMarkupManager; }
        }

        /// <summary>
        /// Use the Markup Manager to update all targets
        /// </summary>
        public static void UpdateAllTargets()
        {
            ResxMarkupManager.UpdateAllTargets();
        }

        /// <summary>
        /// Update the ResxExtension target with the given key
        /// </summary>
        public static void UpdateTarget(string key)
        {
            foreach (ResxExtension target in ResxMarkupManager.ActiveExtensions)
            {
                if (target.Key == key)
                {
                    target.UpdateTarget();
                }
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Check if the assembly contains an embedded resx of the given name
        /// </summary>
        /// <param name="assembly">The assembly to check</param>
        /// <param name="resxName">The name of the resource we are looking for</param>
        /// <returns>True if the assembly contains the resource</returns>
        private static bool HasEmbeddedResx(Assembly assembly, string resxName)
        {
            // Ignore dynamic assemblies
            if (assembly is System.Reflection.Emit.AssemblyBuilder ||
                assembly.ManifestModule is System.Reflection.Emit.ModuleBuilder ||
                assembly.ManifestModule.GetType().FullName == "System.Reflection.Emit.InternalModuleBuilder")
                return false;

            string[] resources = assembly.GetManifestResourceNames();
            string searchName = resxName.ToLower() + ".resources";
            
            return resources.Any(resource => resource.ToLower() == searchName);
        }

        /// <summary>
        /// Find the assembly that contains the type
        /// </summary>
        /// <returns>The assembly if loaded (otherwise null)</returns>
        private static Assembly FindResourceAssembly(string resxName)
        {
            var assembly = Assembly.GetEntryAssembly();

            // check the entry assembly first - this will short circuit a lot of searching
            //
            if (assembly != null && HasEmbeddedResx(assembly, resxName)) return assembly;

            var assemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (var searchAssembly in assemblies)
            {
                // skip system assemblies
                //
                var name = searchAssembly.FullName;
                if (name != null &&
                    !name.StartsWith("Microsoft.") &&
                    !name.StartsWith("System.") &&
                    !name.StartsWith("System,") &&
                    !name.StartsWith("mscorlib,") &&
                    !name.StartsWith("PresentationFramework,") &&
                    !name.StartsWith("WindowsBase,"))
                {
                    if (HasEmbeddedResx(searchAssembly, resxName)) return searchAssembly;
                }
            }

            return null;
        }

        /// <summary>
        /// Get the resource manager for this type
        /// </summary>
        /// <param name="resxName">The name of the embedded resx</param>
        /// <returns>The resource manager</returns>
        /// <remarks>Caches resource managers to improve performance</remarks>
        private static ResourceManager GetResourceManager(string resxName)
        {
            WeakReference reference;
            ResourceManager result = null;
            if (ResourceManagers.TryGetValue(resxName, out reference))
            {
                result = reference.Target as ResourceManager;

                // if the resource manager has been garbage collected then remove the cache
                // entry (it will be readded)
                //
                if (result == null)
                {
                    ResourceManagers.Remove(resxName);
                }
            }

            if (result == null)
            {
                Assembly assembly = FindResourceAssembly(resxName);
                if (assembly != null)
                {
                    result = new ResourceManager(resxName, assembly);
                }
                ResourceManagers.Add(resxName, new WeakReference(result));
            }
            return result;
        }

        [DllImport("gdi32.dll")]
        private static extern bool DeleteObject(IntPtr hObject);

        /// <summary>
        /// Convert a resource object to the type required by the WPF element
        /// </summary>
        /// <param name="value">The resource value to convert</param>
        /// <returns>The WPF element value</returns>
        private object ConvertValue(object value)
        {
            object result;
            BitmapSource bitmapSource = null;

            // convert icons and bitmaps to BitmapSource objects that WPF uses
            if (value is Icon)
            {
                Icon icon = value as Icon;

                // For icons we must create a new BitmapFrame from the icon data stream
                // The approach we use for bitmaps (below) doesn't work when setting the
                // Icon property of a window (although it will work for other Icons)
                //
                using (var iconStream = new MemoryStream())
                {
                    icon.Save(iconStream);
                    iconStream.Seek(0, SeekOrigin.Begin);
                    bitmapSource = BitmapFrame.Create(iconStream);
                }
            }
            else if (value is Bitmap)
            {
                var bitmap = value as Bitmap;
                IntPtr bitmapHandle = bitmap.GetHbitmap();
                bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(
                    bitmapHandle, IntPtr.Zero, Int32Rect.Empty,
                    BitmapSizeOptions.FromEmptyOptions());
                bitmapSource.Freeze();
                DeleteObject(bitmapHandle);
            }

            if (bitmapSource != null)
            {
                // if the target property is expecting the Icon to be content then we
                // create an ImageControl and set its Source property to image
                //
                if (TargetPropertyType == typeof(object))
                {
                    var imageControl = new System.Windows.Controls.Image();
                    imageControl.Source = bitmapSource;
                    imageControl.Width = bitmapSource.Width;
                    imageControl.Height = bitmapSource.Height;
                    result = imageControl;
                }
                else
                {
                    result = bitmapSource;
                }
            }
            else
            {
                result = value;

                // allow for resources to either contain simple strings or typed data
                //
                Type targetType = TargetPropertyType;
                if (value is String && targetType != typeof(String) && targetType != typeof(object))
                {
                    TypeConverter tc = TypeDescriptor.GetConverter(targetType);
                    result = tc.ConvertFromInvariantString(value as string);
                }
            }

            return result;
        }

        /// <summary>
        /// Return the default value for the property
        /// </summary>
        /// <returns></returns>
        private object GetDefaultValue(string key)
        {
            object result = DefaultValue;
            Type targetType = TargetPropertyType;
            if (DefaultValue == null)
            {
                if (targetType == typeof(String) || targetType == typeof(object))
                {
                    result = "#" + key;
                }
            }
            else if (targetType != null)
            {
                // convert the default value if necessary to the required type
                //
                if (targetType != typeof(String) && targetType != typeof(object))
                {
                    try
                    {
                        TypeConverter tc = TypeDescriptor.GetConverter(targetType);
                        result = tc.ConvertFromInvariantString(DefaultValue);
                    }
                    catch
                    {
                    }
                }
            }
            return result;
        }

        /// <summary>
        /// Return the value associated with the key from the resource manager
        /// </summary>
        /// <returns>The value from the resources if possible otherwise the default value</returns>
        protected override object GetValue(object targetObject)
        {
            if (string.IsNullOrEmpty(Key))
                throw new ArgumentException("Key cannot be null.");

            if (IsInDesignMode) return GetDefaultValue(Key);

            if (string.IsNullOrEmpty(ResxName) &&
                string.IsNullOrEmpty(GetGlobalResxName(targetObject as DependencyObject)))
                throw new ArgumentException("ResxName cannot be null.");

            object result = null;
            var resxName = !string.IsNullOrEmpty(ResxName) ? ResxName : GetGlobalResxName(targetObject as DependencyObject);

            try
            {
                object resource = null;
                if (GetResource != null)
                {
                    resource = GetResource(resxName, Key, CultureManager.UICulture);
                }
                if (resource == null)
                {
                    if (m_ResourceManager == null)
                    {
                        m_ResourceManager = GetResourceManager(resxName);
                    }
                    if (m_ResourceManager != null)
                    {
                        resource = m_ResourceManager.GetObject(Key, CultureManager.UICulture);
                    }
                }
                result = ConvertValue(resource);
            }
            catch
            {
            }

            return result ?? GetDefaultValue(Key);
        }

        #endregion
    }
}